﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Loki.Engine;
using Loki.Utils;
using Loki.Messages;

namespace Loki.Model
{
    public class EntityContainer<TEntityClass, TEntityKey> : LokiObject, IDisposable, IContainer<TEntityClass>, IIndexedEnumerable<TEntityClass, TEntityKey>
        where TEntityClass : IAtomicEntity<TEntityKey>
    {
        #region Disposable
        /// <summary>
        /// Releases all resources used by an instance of the <see cref="EntityContainer" /> class.
        /// </summary>
        /// <remarks>
        /// This method calls the virtual <see cref="Dispose(bool)" /> method, passing in <strong>true</strong>, and then suppresses 
        /// finalization of the instance.
        /// </remarks>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged resources before an instance of the <see cref="EntityContainer" /> class is reclaimed by garbage collection.
        /// </summary>
        /// <remarks>
        /// This method releases unmanaged resources by calling the virtual <see cref="Dispose(bool)" /> method, passing in <strong>false</strong>.
        /// </remarks>
        ~EntityContainer()
        {
            Dispose(false);
        }

        /// <summary>
        /// Releases the unmanaged resources used by an instance of the <see cref="EntityContainer" /> class and optionally releases the managed resources.
        /// </summary>
        /// <param name="disposing"><strong>true</strong> to release both managed and unmanaged resources; <strong>false</strong> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {

        }
        #endregion

        #region Fields
        private ReaderWriterLockSlim _Lock = new ReaderWriterLockSlim();
        private Dictionary<string, Func<object, IEnumerable<TEntityClass>>> _IndexLoaders = new Dictionary<string, Func<object, IEnumerable<TEntityClass>>>();
        private Func<TEntityKey, TEntityClass> _PKLoader;
        private Func<IEnumerable<TEntityClass>> _GlobalLoader;
        private Func<TEntityKey, TEntityClass> _DefaultGetter;
        private ContainerConfiguration<TEntityClass, TEntityKey> _Config;
        private bool _Loaded = false;
        private Dictionary<string, IKeyMapper<TEntityClass>> _Indexes = new Dictionary<string, IKeyMapper<TEntityClass>>();
        private UniqueKeyMapper<TEntityClass, TEntityKey> _PK;
        #endregion

        public EntityContainer(ContainerConfiguration<TEntityClass, TEntityKey> P_Configuration)
        {
            _Config = P_Configuration;

            // configure primary key
            _PK = new UniqueKeyMapper<TEntityClass, TEntityKey>(_Config.PrimaryKey.Indexer, _Config.PrimaryKey.Comparer);
            _PKLoader = _Config.PrimaryKey.Loader;

            _GlobalLoader = _Config.FullLoader.Compile();
            if (_Config.PrimaryKey.Default != null)
            {
                _DefaultGetter = _Config.PrimaryKey.Default;
            }

            // other indexes
            if (_Config.Indexes != null)
            {
                // configure indexes
                foreach (IIndexConfiguration<TEntityClass> L_IndexConfig in _Config.Indexes)
                {
                    PropertyInfo L_IndexerInfo = ExpressionHelper.Reflect<TEntityClass>.GetProperty(L_IndexConfig.Indexer);
                    string L_PropertyName = L_IndexerInfo.Name;
                    Type L_FKType = L_IndexerInfo.PropertyType;
                    IEqualityComparer<TEntityClass> L_Comparer = null;

                    L_Comparer = new EntityEqualityComparer<TEntityClass, TEntityKey>();

                    Type L_MapperType = typeof(IndexedKeyMapper<,>).MakeGenericType(new System.Type[] { typeof(TEntityClass), L_FKType });

                    _Indexes[L_PropertyName] = (IKeyMapper<TEntityClass>)Activator.CreateInstance(L_MapperType, L_IndexConfig, L_Comparer);

                    // register dedicated loader
                    if (L_IndexConfig.Loader != null)
                    {
                        _IndexLoaders[L_PropertyName] = L_IndexConfig.Loader;
                    }
                }
            }
        }

        #region Global load and remove
        public void FullLoad()
        {
            _Lock.WithWrite(_FullLoad);
        }

        public void Reset()
        {
            _Lock.WithWrite(_Reset);
            OnCollectionChanged(_ResetArgs);
        }

        private static NotifyCollectionChangedEventArgs _ResetArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);

        private void _Reset()
        {
            _PK.Reset();
            foreach (IKeyMapper<TEntityClass> L_Mapper in _Indexes.Values)
            {
                L_Mapper.Reset();
            }
            _Loaded = false;
        }
        #endregion

        #region Getters
        public TEntityClass this[TEntityKey P_Index]
        {
            get { return this.Get(P_Index); }
        }

        public IEnumerable<TEntityClass> this[string P_FKName, object P_FK]
        {
            get { return GetAllBy(P_FKName, P_FK); }
        }

        /// <summary>
        /// Gets the <see cref="System.Collections.Generic.IEnumerable&lt;TEntityClass&gt;"/> with the specified value on the specified member.
        /// </summary>
        /// <param name="P_FKSelector">The FK selector.</param>
        /// <param name="P_FK">The member value</param>
        /// <returns></returns>
        public IEnumerable<TEntityClass> GetAllBy(string P_FKName, object P_FKValue)
        {
            if (_Indexes.ContainsKey(P_FKName))
            {
                return _Lock.WithReadWrite(
                    () => !_CheckFKLoaded(P_FKName, P_FKValue),
                    () => { _LoadFK(P_FKName, P_FKValue); return _GetAllBy(P_FKName, P_FKValue); },
                    () => _GetAllBy(P_FKName, P_FKValue));
            }
            else
            {
                throw BuildErrorFormat<ArgumentException>(ToolsErrors.TOO_120, P_FKName, typeof(TEntityClass).Name);
            }
        }

        /// <summary>
        /// Gets the entity with the specified primary key.
        /// </summary>
        /// <param name="P_Key">The key</param>
        /// <returns></returns>
        public TEntityClass Get(TEntityKey P_Key)
        {
            return _Lock.WithReadWrite(
                () => !_CheckPKLoaded(P_Key),
                () => { _LoadPK(P_Key); return _Get(P_Key); },
                () => _Get(P_Key));
        }

        private IEnumerable<TEntityClass> _GetAll()
        {
            return _PK.ToArray();
        }

        private TEntityClass _Get(TEntityKey P_Key)
        {
            return _PK.Get(P_Key);
        }

        private IEnumerable<TEntityClass> _GetAllBy(string P_FKName, object P_FKValue)
        {
            return _Indexes[P_FKName].GetAllByKey(P_FKValue).ToArray();
        }
        #endregion

        #region Registering
        /// <summary>
        /// Registers the specified entity.
        /// </summary>
        /// <param name="P_Entity">The entity</param>
        public bool Register(TEntityClass P_Entity)
        {
            return _Lock.WithWrite(() => A_AddOrReplaceItem(P_Entity));
        }

        private bool A_AddOrReplaceItem(TEntityClass P_Entity)
        {
            bool L_AlreadyPresent;
            NotifyCollectionChangedEventArgs e = null;

            L_AlreadyPresent = _IsLoaded(P_Entity);
            if (L_AlreadyPresent)
            {
                e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, P_Entity, _PK.Get(GetKey(P_Entity)));
            }
            else
            {
                e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, P_Entity);
            }

            // registering
            _IndexItem(P_Entity);

            OnCollectionChanged(e);
            return !L_AlreadyPresent;
        }

        private void _IndexItem(TEntityClass P_Entity)
        {
            _PK.Register(P_Entity);

            Parallel.ForEach(_Indexes.Values, (index) =>
            {
                index.Register(P_Entity);
            });
        }

        private void _IndexItems(IEnumerable<TEntityClass> P_Entities)
        {
            // collect one set value (if not it will be called in every thread
            var L_Entities = P_Entities.ToArray();

            foreach (var L_Entity in L_Entities)
            {
                _PK.Register(L_Entity);
            }

            Parallel.ForEach(_Indexes.Values, (index) =>
            {
                foreach (var L_Entity in L_Entities)
                {
                    index.Register(L_Entity);
                }
            });
        }

        private bool _IsLoaded(TEntityClass P_Entity)
        {
            TEntityKey L_Key = (TEntityKey)_PK.ComputeIndex(P_Entity);
            return _PK.IsLoaded(L_Key);
        }

        private TEntityKey GetKey(TEntityClass P_Entity)
        {
            return (TEntityKey)_PK.ComputeIndex(P_Entity);
        }

        public void Remove(TEntityClass P_Entity)
        {
            _Lock.WithWrite(() => _Remove(P_Entity));
        }

        private void _Remove(TEntityClass P_Entity)
        {
            TEntityKey L_Key = (TEntityKey)_PK.ComputeIndex(P_Entity);

            // remove
            _PK.Remove(P_Entity);
            Parallel.ForEach(_Indexes.Values, (index) =>
            {
                index.Remove(P_Entity);
            });

            Type L_Type = typeof(TEntityClass);
            Log.InfoFormat("Removing entity {0} of L_Type {1}", L_Key, L_Type.Name);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, P_Entity));
        }
        #endregion

        #region Loading checks
        public void ReloadKey(Expression<Func<TEntityClass, object>> P_FKSelector, object P_FkValue)
        {
            _Lock.WithWrite(() => _ReloadKey(P_FKSelector, P_FkValue));

            OnCollectionChanged(_ResetArgs);
        }

        private void _ReloadKey(Expression<Func<TEntityClass, object>> P_FKSelector, object P_FkValue)
        {
            string L_FKName = ExpressionHelper.Reflect<TEntityClass>.GetProperty(P_FKSelector).Name;
            IEnumerable<TEntityClass> L_OldItems = _GetAllBy(L_FKName, P_FkValue).ToList();

            // remove PK
            foreach (var L_OldItem in L_OldItems)
            {
                _PK.Remove(L_OldItem);
            }

            // remove items
            Parallel.ForEach(_Indexes.Values, (index) =>
            {
                foreach (var L_OldItem in L_OldItems)
                {
                    index.Remove(L_OldItem);
                }
            });

            // reload
            _LoadFK(L_FKName, P_FkValue);
        }

        private bool _CheckPKLoaded(TEntityKey P_Key)
        {
            return !Object.Equals(_PK.Get(P_Key), default(TEntityClass));
        }

        private void _LoadPK(TEntityKey P_Key)
        {
            // entity not registerd, try loading
            TEntityClass L_Return = default(TEntityClass);
            try
            {
                L_Return = _PKLoader(P_Key);
            }
            catch
            {
                throw;
                // TODO : improve
                // throw BuildError(_Config.LoadingErrorCode, ex);
            }

            // check if entity is found
            if (!object.Equals(L_Return, default(TEntityClass)))
            {
                A_AddOrReplaceItem(L_Return);
            }
            // not found error if specified
            else if (_Config.PrimaryKey.NotFoundErrorCode != null)
            {
                throw BuildErrorFormat<ArgumentException>("Primary Key not fund : {0}", P_Key);
            }
            // load default value for not recurrent calls
            else if (_DefaultGetter != null)
            {
                A_AddOrReplaceItem(_DefaultGetter(P_Key));
            }
        }

        /// <summary>
        /// Checks if the container is full loaded.
        /// </summary>
        /// <returns>Returns <c>true</c> if the container is full, <c>false</c> otherwise.</returns>
        private bool _CheckFullLoad()
        {
            return _Loaded;
        }

        /// <summary>
        /// Loads all elements.
        /// </summary>
        private void _FullLoad()
        {
            foreach (var L_Index in _Indexes)
            {
                L_Index.Value.Reset();
            }

            _IndexItems(_GlobalLoader());

            _Loaded = true;
            OnCollectionChanged(_ResetArgs);
        }

        private bool _CheckFKLoaded(string P_FKName, object P_FKValue)
        {
            return _Loaded || _Indexes[P_FKName].IsLoaded(P_FKValue);
        }

        private void _LoadFK(string P_FKName, object P_FKValue)
        {
            // check if a the key is loaded and if a specific loader is available
            if (_IndexLoaders.ContainsKey(P_FKName) && !_Indexes[P_FKName].IsLoaded(P_FKValue))
            {
                try
                {
                    IEnumerable<TEntityClass> _Entities = _IndexLoaders[P_FKName](P_FKValue);
                    _IndexItems(_Entities);
                    Log.DebugFormat("Validated {0} {1} on {2} container", P_FKName, P_FKValue, typeof(TEntityClass).Name);
                    _Indexes[P_FKName].MarkAsLoaded(P_FKValue);

                    OnCollectionChanged(_ResetArgs);
                }
                catch
                {
                    throw;
                    // throw BuildError(_Config.LoadingErrorCode, ex);
                }
            }
            else
            {
                // no specific loader defined
                _FullLoad();
            }
        }
        #endregion

        #region IEnumerable
        /// <summary>
        /// Retourne un énumérateur qui parcourt la collection.
        /// </summary>
        /// <returns>
        ///   <see cref="T:System.Collections.Generic.IEnumerator`1"/> pouvant être utilisé pour parcourir la collection.
        /// </returns>
        public IEnumerator<TEntityClass> GetEnumerator()
        {
            var L_List = _Lock.WithReadWrite(
                () => !_CheckFullLoad(),
                () => { _FullLoad(); return _GetAll(); },
                () => _GetAll());

            foreach (var L_Element in L_List)
            {
                yield return L_Element;
            }
        }

        /// <summary>
        /// Retourne un énumérateur qui itère au sein d'une collection.
        /// </summary>
        /// <returns>
        /// Objet <see cref="T:System.Collections.IEnumerator"/> pouvant être utilisé pour itérer au sein de la collection.
        /// </returns>
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }
        #endregion

        #region Events
        #region Collection Changed
        private readonly object CollectionChangedEventLock = new object();
        private NotifyCollectionChangedEventHandler CollectionChangedEvent;

        public event NotifyCollectionChangedEventHandler CollectionChanged
        {
            add
            {
                lock (CollectionChangedEventLock)
                {
                    CollectionChangedEvent += value;
                }
            }
            remove
            {
                lock (CollectionChangedEventLock)
                {
                    CollectionChangedEvent -= value;
                }
            }
        }

        protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            NotifyCollectionChangedEventHandler handler = null;

            lock (CollectionChangedEventLock)
            {
                handler = CollectionChangedEvent;

                if (handler == null)
                    return;
            }
            Task.Factory.StartNew(() => handler(this, e));
        }

        #endregion
        #endregion
    }
}
